(function (factory) {
  factory(window);
  if (typeof define == 'function' && define.amd) {
    define(function () {
      return Recorder;
    });
  }
  if (typeof module == 'object' && module.exports) {
    module.exports = Recorder;
  }
})(function (window) {
  var NOOP = function () {};

  var Recorder = function (set) {
    return new initFn(set);
  };

  var RecTxt = 'Recorder';
  var getUserMediaTxt = 'getUserMedia';
  var srcSampleRateTxt = 'srcSampleRate';
  var sampleRateTxt = 'sampleRate';

  //是否已经打开了全局的麦克风录音，所有工作都已经准备好了，就等接收音频数据了
  Recorder.IsOpen = function () {
    var stream = Recorder.Stream;
    if (stream) {
      var tracks =
        (stream.getTracks && stream.getTracks()) || stream.audioTracks || [];
      var track = tracks[0];
      if (track) {
        var state = track.readyState;
        return state == 'live' || state == track.LIVE;
      }
    }
    return false;
  };
  /*H5录音时的AudioContext缓冲大小。会影响H5录音时的onProcess调用速率，相对于AudioContext.sampleRate=48000时，4096接近12帧/s，调节此参数可生成比较流畅的回调动画。
	取值256, 512, 1024, 2048, 4096, 8192, or 16384
	注意，取值不能过低，2048开始不同浏览器可能回调速率跟不上造成音质问题。
	一般无需调整，调整后需要先close掉已打开的录音，再open时才会生效。
*/
  Recorder.BufferSize = 4096;
  //销毁已持有的所有全局资源，当要彻底移除Recorder时需要显式的调用此方法
  Recorder.Destroy = function () {
    CLog(RecTxt + ' Destroy');
    Disconnect(); //断开可能存在的全局Stream、资源

    for (var k in DestroyList) {
      DestroyList[k]();
    }
  };
  var DestroyList = {};
  //登记一个需要销毁全局资源的处理方法
  Recorder.BindDestroy = function (key, call) {
    DestroyList[key] = call;
  };
  //判断浏览器是否支持录音，随时可以调用。注意：仅仅是检测浏览器支持情况，不会判断和调起用户授权，不会判断是否支持特定格式录音。
  Recorder.Support = function () {
    var AC = window.AudioContext;
    if (!AC) {
      AC = window.webkitAudioContext;
    }
    if (!AC) {
      return false;
    }
    var scope = navigator.mediaDevices || {};
    if (!scope[getUserMediaTxt]) {
      scope = navigator;
      scope[getUserMediaTxt] ||
        (scope[getUserMediaTxt] =
          scope.webkitGetUserMedia ||
          scope.mozGetUserMedia ||
          scope.msGetUserMedia);
    }
    if (!scope[getUserMediaTxt]) {
      return false;
    }

    Recorder.Scope = scope;
    if (!Recorder.Ctx || Recorder.Ctx.state == 'closed') {
      //不能反复构造，低版本number of hardware contexts reached maximum (6)
      Recorder.Ctx = new AC();

      Recorder.BindDestroy('Ctx', function () {
        var ctx = Recorder.Ctx;
        if (ctx && ctx.close) {
          //能关掉就关掉，关不掉就保留着
          ctx.close();
          Recorder.Ctx = 0;
        }
      });
    }
    return true;
  };

  /*是否启用MediaRecorder.WebM.PCM来进行音频采集连接（如果浏览器支持的话），默认启用，禁用或者不支持时将使用AudioWorklet或ScriptProcessor来连接；MediaRecorder采集到的音频数据比其他方式更好，几乎不存在丢帧现象，所以音质明显会好很多，建议保持开启*/
  var ConnectEnableWebM = 'ConnectEnableWebM';
  Recorder[ConnectEnableWebM] = true;

  /*是否启用AudioWorklet特性来进行音频采集连接（如果浏览器支持的话），默认禁用，禁用或不支持时将使用过时的ScriptProcessor来连接（如果方法还在的话），当前AudioWorklet的实现在移动端没有ScriptProcessor稳健；ConnectEnableWebM如果启用并且有效时，本参数将不起作用*/
  var ConnectEnableWorklet = 'ConnectEnableWorklet';
  Recorder[ConnectEnableWorklet] = false;

  /*初始化H5音频采集连接。如果自行提供了sourceStream将只进行一次简单的连接处理。如果是普通麦克风录音，此时的Stream是全局的，Safari上断开后就无法再次进行连接使用，表现为静音，因此使用全部使用全局处理避免调用到disconnect；全局处理也有利于屏蔽底层细节，start时无需再调用底层接口，提升兼容、可靠性。*/
  var Connect = function (streamStore, isUserMedia) {
    var bufferSize = streamStore.BufferSize || Recorder.BufferSize;

    var ctx = Recorder.Ctx,
      stream = streamStore.Stream;
    var mediaConn = function (node) {
      var media = (stream._m = ctx.createMediaStreamSource(stream));
      var ctxDest = ctx.destination,
        cmsdTxt = 'createMediaStreamDestination';
      if (ctx[cmsdTxt]) {
        ctxDest = ctx[cmsdTxt]();
      }
      media.connect(node);
      node.connect(ctxDest);
    };
    var isWebM,
      isWorklet,
      badInt,
      webMTips = '';
    var calls = stream._call;

    //浏览器回传的音频数据处理
    var onReceive = function (float32Arr) {
      for (var k0 in calls) {
        //has item
        var size = float32Arr.length;

        var pcm = new Int16Array(size);
        var sum = 0;
        for (var j = 0; j < size; j++) {
          //floatTo16BitPCM
          var s = Math.max(-1, Math.min(1, float32Arr[j]));
          s = s < 0 ? s * 0x8000 : s * 0x7fff;
          pcm[j] = s;
          sum += Math.abs(s);
        }

        for (var k in calls) {
          calls[k](pcm, sum);
        }

        return;
      }
    };

    var scriptProcessor = 'ScriptProcessor'; //一堆字符串名字，有利于压缩js
    var audioWorklet = 'audioWorklet';
    var recAudioWorklet = RecTxt + ' ' + audioWorklet;
    var RecProc = 'RecProc';
    var MediaRecorderTxt = 'MediaRecorder';
    var MRWebMPCM = MediaRecorderTxt + '.WebM.PCM';

    //===================连接方式三=========================
    //古董级别的 ScriptProcessor 处理，目前所有浏览器均兼容，虽然是过时的方法，但更稳健，移动端性能比AudioWorklet强
    var oldFn = ctx.createScriptProcessor || ctx.createJavaScriptNode;
    var oldIsBest =
      '。由于' +
      audioWorklet +
      '内部1秒375次回调，在移动端可能会有性能问题导致回调丢失录音变短，PC端无影响，暂不建议开启' +
      audioWorklet +
      '。';
    var oldScript = function () {
      isWorklet = stream.isWorklet = false;
      _Disconn_n(stream);
      CLog(
        'Connect采用老的' +
          scriptProcessor +
          '，' +
          (Recorder[ConnectEnableWorklet] ? '但已' : '可') +
          '设置' +
          RecTxt +
          '.' +
          ConnectEnableWorklet +
          '=true尝试启用' +
          audioWorklet +
          webMTips +
          oldIsBest,
        3
      );

      var process = (stream._p = oldFn.call(ctx, bufferSize, 1, 1)); //单声道，省的数据处理复杂
      mediaConn(process);

      var _DsetTxt = '_D220626',
        _Dset = Recorder[_DsetTxt];
      if (_Dset) CLog('Use ' + RecTxt + '.' + _DsetTxt, 3);
      process.onaudioprocess = function (e) {
        var arr = e.inputBuffer.getChannelData(0);
        if (_Dset) {
          //临时调试用的参数，未来会被删除
          arr = new Float32Array(arr); //块是共享的，必须复制出来
          setTimeout(function () {
            onReceive(arr);
          }); //立即退出回调，试图减少对浏览器录音的影响
        } else {
          onReceive(arr);
        }
      };
    };

    //===================连接方式二=========================
    var connWorklet = function () {
      //尝试开启AudioWorklet处理
      isWebM = stream.isWebM = false;
      _Disconn_r(stream);

      isWorklet = stream.isWorklet = !oldFn || Recorder[ConnectEnableWorklet];
      var AwNode = window.AudioWorkletNode;
      if (!(isWorklet && ctx[audioWorklet] && AwNode)) {
        oldScript(); //被禁用 或 不支持，直接使用老的
        return;
      }
      var clazzUrl = function () {
        var xf = function (f) {
          return f
            .toString()
            .replace(/^function|DEL_/g, '')
            .replace(/\$RA/g, recAudioWorklet);
        };
        var clazz = 'class ' + RecProc + ' extends AudioWorkletProcessor{';
        clazz +=
          'constructor ' +
          xf(function (option) {
            DEL_super(option);
            var This = this,
              bufferSize = option.processorOptions.bufferSize;
            This.bufferSize = bufferSize;
            This.buffer = new Float32Array(bufferSize * 2); //乱给size搞乱缓冲区不管
            This.pos = 0;
            This.port.onmessage = function (e) {
              if (e.data.kill) {
                This.kill = true;
                console.log('$RA kill call');
              }
            };
            console.log('$RA .ctor call', option);
          });

        //https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process 每次回调128个采样数据，1秒375次回调，高频导致移动端性能问题，结果就是回调次数缺斤少两，进而导致丢失数据，PC端似乎没有性能问题
        clazz +=
          'process ' +
          xf(function (input, b, c) {
            //需要等到ctx激活后才会有回调
            var This = this,
              bufferSize = This.bufferSize;
            var buffer = This.buffer,
              pos = This.pos;
            input = (input[0] || [])[0] || [];
            if (input.length) {
              buffer.set(input, pos);
              pos += input.length;

              var len = ~~(pos / bufferSize) * bufferSize;
              if (len) {
                this.port.postMessage({ val: buffer.slice(0, len) });

                var more = buffer.subarray(len, pos);
                buffer = new Float32Array(bufferSize * 2);
                buffer.set(more);
                pos = more.length;
                This.buffer = buffer;
              }
              This.pos = pos;
            }
            return !This.kill;
          });
        clazz +=
          '}' +
          'try{' +
          'registerProcessor("' +
          RecProc +
          '", ' +
          RecProc +
          ')' +
          '}catch(e){' +
          'console.error("' +
          recAudioWorklet +
          '注册失败",e)' +
          '}';
        //URL.createObjectURL 本地有些浏览器会报 Not allowed to load local resource，直接用dataurl
        return (
          'data:text/javascript;base64,' +
          btoa(unescape(encodeURIComponent(clazz)))
        );
      };

      var awNext = function () {
        //可以继续，没有调用断开
        return isWorklet && stream._na;
      };
      var nodeAlive = (stream._na = function () {
        //start时会调用，只要没有收到数据就断定AudioWorklet有问题，恢复用老的
        if (badInt !== '') {
          //没有回调过数据
          clearTimeout(badInt);
          badInt = setTimeout(function () {
            badInt = 0;
            if (awNext()) {
              CLog(
                audioWorklet + '未返回任何音频，恢复使用' + scriptProcessor,
                3
              );
              oldFn && oldScript(); //未来没有老的，可能是误判
            }
          }, 500);
        }
      });
      var createNode = function () {
        if (!awNext()) return;
        var node = (stream._n = new AwNode(ctx, RecProc, {
          processorOptions: { bufferSize: bufferSize },
        }));
        mediaConn(node);
        node.port.onmessage = function (e) {
          if (badInt) {
            clearTimeout(badInt);
            badInt = '';
          }
          if (awNext()) {
            onReceive(e.data.val);
          } else if (!isWorklet) {
            CLog(audioWorklet + '多余回调', 3);
          }
        };
        CLog(
          'Connect采用' +
            audioWorklet +
            '，设置' +
            RecTxt +
            '.' +
            ConnectEnableWorklet +
            '=false可恢复老式' +
            scriptProcessor +
            webMTips +
            oldIsBest,
          3
        );
      };

      //如果start时的resume和下面的构造node同时进行，将会导致部分浏览器崩溃，源码assets中 ztest_chrome_bug_AudioWorkletNode.html 可测试。所以，将所有代码套到resume里面（不管catch），避免出现这个问题
      ctx.resume()[calls && 'finally'](function () {
        //注释掉这行 观摩浏览器崩溃 STATUS_ACCESS_VIOLATION
        if (!awNext()) return;
        if (ctx[RecProc]) {
          createNode();
          return;
        }
        var url = clazzUrl();
        ctx[audioWorklet]
          .addModule(url)
          .then(function (e) {
            if (!awNext()) return;
            ctx[RecProc] = 1;
            createNode();
            if (badInt) {
              //重新计时
              nodeAlive();
            }
          })
          [calls && 'catch'](function (e) {
            //fix 关键字，保证catch压缩时保持字符串形式
            CLog(audioWorklet + '.addModule失败', 1, e);
            awNext() && oldScript();
          });
      });
    };

    //===================连接方式一=========================
    var connWebM = function () {
      //尝试开启MediaRecorder录制webm+pcm处理
      var MR = window[MediaRecorderTxt];
      var onData = 'ondataavailable';
      var webmType = 'audio/webm; codecs=pcm';
      isWebM = stream.isWebM = Recorder[ConnectEnableWebM];

      var supportMR =
        MR && onData in MR.prototype && MR.isTypeSupported(webmType);
      webMTips = supportMR ? '' : '（此浏览器不支持' + MRWebMPCM + '）';
      if (!isUserMedia || !isWebM || !supportMR) {
        connWorklet(); //非麦克风录音（MediaRecorder采样率不可控） 或 被禁用 或 不支持MediaRecorder 或 不支持webm+pcm
        return;
      }

      var mrNext = function () {
        //可以继续，没有调用断开
        return isWebM && stream._ra;
      };
      var mrAlive = (stream._ra = function () {
        //start时会调用，只要没有收到数据就断定MediaRecorder有问题，降级处理
        if (badInt !== '') {
          //没有回调过数据
          clearTimeout(badInt);
          badInt = setTimeout(function () {
            //badInt=0; 保留给nodeAlive继续判断
            if (mrNext()) {
              CLog(
                MediaRecorderTxt + '未返回任何音频，降级使用' + audioWorklet,
                3
              );
              connWorklet();
            }
          }, 500);
        }
      });

      var mrSet = Object.assign(
        { mimeType: webmType },
        Recorder.ConnectWebMOptions
      );
      var mr = (stream._r = new MR(stream, mrSet));
      var webmData = (stream._rd = { sampleRate: ctx[sampleRateTxt] });
      mr[onData] = function (e) {
        //提取webm中的pcm数据，提取失败就等着badInt超时降级处理
        var reader = new FileReader();
        reader.onloadend = function () {
          if (mrNext()) {
            var f32arr = WebM_Extract(new Uint8Array(reader.result), webmData);
            if (!f32arr) return;
            if (f32arr == -1) {
              //无法提取，立即降级
              connWorklet();
              return;
            }

            if (badInt) {
              clearTimeout(badInt);
              badInt = '';
            }
            onReceive(f32arr);
          } else if (!isWebM) {
            CLog(MediaRecorderTxt + '多余回调', 3);
          }
        };
        reader.readAsArrayBuffer(e.data);
      };
      mr.start(~~(bufferSize / 48)); //按48k时的回调间隔
      CLog(
        'Connect采用' +
          MRWebMPCM +
          '，设置' +
          RecTxt +
          '.' +
          ConnectEnableWebM +
          '=false可恢复使用' +
          audioWorklet +
          '或老式' +
          scriptProcessor
      );
    };

    connWebM();
  };
  var ConnAlive = function (stream) {
    if (stream._na) stream._na(); //检查AudioWorklet连接是否有效，无效就回滚到老的ScriptProcessor
    if (stream._ra) stream._ra(); //检查MediaRecorder连接是否有效，无效就降级处理
  };
  var _Disconn_n = function (stream) {
    stream._na = null;
    if (stream._n) {
      stream._n.port.postMessage({ kill: true });
      stream._n.disconnect();
      stream._n = null;
    }
  };
  var _Disconn_r = function (stream) {
    stream._ra = null;
    if (stream._r) {
      stream._r.stop();
      stream._r = null;
    }
  };
  var Disconnect = function (streamStore) {
    streamStore = streamStore || Recorder;
    var isGlobal = streamStore == Recorder;

    var stream = streamStore.Stream;
    if (stream) {
      if (stream._m) {
        stream._m.disconnect();
        stream._m = null;
      }
      if (stream._p) {
        stream._p.disconnect();
        stream._p.onaudioprocess = stream._p = null;
      }
      _Disconn_n(stream);
      _Disconn_r(stream);

      if (isGlobal) {
        //全局的时候，要把流关掉（麦克风），直接提供的流不处理
        var tracks =
          (stream.getTracks && stream.getTracks()) || stream.audioTracks || [];
        for (var i = 0; i < tracks.length; i++) {
          var track = tracks[i];
          track.stop && track.stop();
        }
        stream.stop && stream.stop();
      }
    }
    streamStore.Stream = 0;
  };

  /*对pcm数据的采样率进行转换
pcmDatas: [[Int16,...]] pcm片段列表
pcmSampleRate:48000 pcm数据的采样率
newSampleRate:16000 需要转换成的采样率，newSampleRate>=pcmSampleRate时不会进行任何处理，小于时会进行重新采样
prevChunkInfo:{} 可选，上次调用时的返回值，用于连续转换，本次调用将从上次结束位置开始进行处理。或可自行定义一个ChunkInfo从pcmDatas指定的位置开始进行转换
option:{ 可选，配置项
		frameSize:123456 帧大小，每帧的PCM Int16的数量，采样率转换后的pcm长度为frameSize的整数倍，用于连续转换。目前仅在mp3格式时才有用，frameSize取值为1152，这样编码出来的mp3时长和pcm的时长完全一致，否则会因为mp3最后一帧录音不够填满时添加填充数据导致mp3的时长变长。
		frameType:"" 帧类型，一般为rec.set.type，提供此参数时无需提供frameSize，会自动使用最佳的值给frameSize赋值，目前仅支持mp3=1152(MPEG1 Layer3的每帧采采样数)，其他类型=1。
			以上两个参数用于连续转换时使用，最多使用一个，不提供时不进行帧的特殊处理，提供时必须同时提供prevChunkInfo才有作用。最后一段数据处理时无需提供帧大小以便输出最后一丁点残留数据。
	}

返回ChunkInfo:{
	//可定义，从指定位置开始转换到结尾
	index:0 pcmDatas已处理到的索引
	offset:0.0 已处理到的index对应的pcm中的偏移的下一个位置
	
	//仅作为返回值
	frameNext:null||[Int16,...] 下一帧的部分数据，frameSize设置了的时候才可能会有
	sampleRate:16000 结果的采样率，<=newSampleRate
	data:[Int16,...] 转换后的PCM结果；如果是连续转换，并且pcmDatas中并没有新数据时，data的长度可能为0
}
*/
  Recorder.SampleData = function (
    pcmDatas,
    pcmSampleRate,
    newSampleRate,
    prevChunkInfo,
    option
  ) {
    prevChunkInfo || (prevChunkInfo = {});
    var index = prevChunkInfo.index || 0;
    var offset = prevChunkInfo.offset || 0;

    var frameNext = prevChunkInfo.frameNext || [];
    option || (option = {});
    var frameSize = option.frameSize || 1;
    if (option.frameType) {
      frameSize = option.frameType == 'mp3' ? 1152 : 1;
    }

    var nLen = pcmDatas.length;
    if (index > nLen + 1) {
      CLog('SampleData似乎传入了未重置chunk ' + index + '>' + nLen, 3);
    }
    var size = 0;
    for (var i = index; i < nLen; i++) {
      size += pcmDatas[i].length;
    }
    size = Math.max(0, size - Math.floor(offset));

    //采样
    var step = pcmSampleRate / newSampleRate;
    if (step > 1) {
      //新采样低于录音采样，进行抽样
      size = Math.floor(size / step);
    } else {
      //新采样高于录音采样不处理，省去了插值处理
      step = 1;
      newSampleRate = pcmSampleRate;
    }

    size += frameNext.length;
    var res = new Int16Array(size);
    var idx = 0;
    //添加上一次不够一帧的剩余数据
    for (var i = 0; i < frameNext.length; i++) {
      res[idx] = frameNext[i];
      idx++;
    }
    //处理数据
    for (; index < nLen; index++) {
      var o = pcmDatas[index];
      var i = offset,
        il = o.length;
      while (i < il) {
        //res[idx]=o[Math.round(i)]; 直接简单抽样

        //当前点=当前点+到后面一个点之间的增量，音质比直接简单抽样好些
        var before = Math.floor(i);
        var after = Math.ceil(i);
        var atPoint = i - before;

        var beforeVal = o[before];
        var afterVal =
          after < il
            ? o[after]
            : //后个点越界了，查找下一个数组
              (pcmDatas[index + 1] || [beforeVal])[0] || 0;
        res[idx] = beforeVal + (afterVal - beforeVal) * atPoint;

        idx++;
        i += step; //抽样
      }
      offset = i - il;
    }
    //帧处理
    frameNext = null;
    var frameNextSize = res.length % frameSize;
    if (frameNextSize > 0) {
      var u8Pos = (res.length - frameNextSize) * 2;
      frameNext = new Int16Array(res.buffer.slice(u8Pos));
      res = new Int16Array(res.buffer.slice(0, u8Pos));
    }

    return {
      index: index,
      offset: offset,

      frameNext: frameNext,
      sampleRate: newSampleRate,
      data: res,
    };
  };

  /*计算音量百分比的一个方法
pcmAbsSum: pcm Int16所有采样的绝对值的和
pcmLength: pcm长度
返回值：0-100，主要当做百分比用
注意：这个不是分贝，因此没用volume当做名称*/
  Recorder.PowerLevel = function (pcmAbsSum, pcmLength) {
    /*计算音量
	更高灵敏度算法:
		限定最大感应值10000
			线性曲线：低音量不友好
				power/10000*100 
			对数曲线：低音量友好，但需限定最低感应值
				(1+Math.log10(power/10000))*100
	*/
    var power = pcmAbsSum / pcmLength || 0; //NaN
    var level;
    if (power < 1251) {
      //1250的结果10%，更小的音量采用线性取值
      level = Math.round((power / 1250) * 10);
    } else {
      level = Math.round(
        Math.min(
          100,
          Math.max(0, (1 + Math.log(power / 10000) / Math.log(10)) * 100)
        )
      );
    }
    return level;
  };

  //带时间的日志输出，CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn，否则当做内容输出，第一个参数不能是对象因为要拼接时间，后面可以接无数个输出参数
  var CLog = function (msg, err) {
    var now = new Date();
    var t =
      ('0' + now.getMinutes()).substr(-2) +
      ':' +
      ('0' + now.getSeconds()).substr(-2) +
      '.' +
      ('00' + now.getMilliseconds()).substr(-3);
    var recID = this && this.envIn && this.envCheck && this.id;
    var arr = ['[' + t + ' ' + RecTxt + (recID ? ':' + recID : '') + ']' + msg];
    var a = arguments,
      console = window.console || {};
    var i = 2,
      fn = console.log;
    if (typeof err == 'number') {
      fn = err == 1 ? console.error : err == 3 ? console.warn : fn;
    } else {
      i = 1;
    }
    for (; i < a.length; i++) {
      arr.push(a[i]);
    }
    if (IsLoser) {
      //古董浏览器，仅保证基本的可执行不代码异常
      fn && fn('[IsLoser]' + arr[0], arr.length > 1 ? arr : '');
    } else {
      fn.apply(console, arr);
    }
  };
  var IsLoser = true;
  try {
    IsLoser = !console.log.apply;
  } catch (e) {}
  Recorder.CLog = CLog;

  var ID = 0;
  function initFn(set) {
    this.id = ++ID;

    var o = {
      type: 'mp3', //输出类型：mp3,wav，wav输出文件尺寸超大不推荐使用，但mp3编码支持会导致js文件超大，如果不需支持mp3可以使js文件大幅减小
      bitRate: 16, //比特率 wav:16或8位，MP3：8kbps 1k/s，8kbps 2k/s 录音文件很小

      sampleRate: 16000, //采样率，wav格式大小=sampleRate*时间；mp3此项对低比特率有影响，高比特率几乎无影响。
      //wav任意值，mp3取值范围：48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000

      onProcess: NOOP, //fn(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd) buffers=[[Int16,...],...]：缓冲的PCM数据，为从开始录音到现在的所有pcm片段；powerLevel：当前缓冲的音量级别0-100，bufferDuration：已缓冲时长，bufferSampleRate：缓冲使用的采样率（当type支持边录边转码(Worker)时，此采样率和设置的采样率相同，否则不一定相同）；newBufferIdx:本次回调新增的buffer起始索引；asyncEnd:fn() 如果onProcess是异步的(返回值为true时)，处理完成时需要调用此回调，如果不是异步的请忽略此参数，此方法回调时必须是真异步（不能真异步时需用setTimeout包裹）。onProcess返回值：如果返回true代表开启异步模式，在某些大量运算的场合异步是必须的，必须在异步处理完成时调用asyncEnd(不能真异步时需用setTimeout包裹)，在onProcess执行后新增的buffer会全部替换成空数组，因此本回调开头应立即将newBufferIdx到本次回调结尾位置的buffer全部保存到另外一个数组内，处理完成后写回buffers中本次回调的结尾位置。

      //*******高级设置******
      // sourceStream:MediaStream Object
      //可选直接提供一个媒体流，从这个流中录制、实时处理音频数据（当前Recorder实例独享此流）；不提供时为普通的麦克风录音，由getUserMedia提供音频流（所有Recorder实例共享同一个流）
      //比如：audio、video标签dom节点的captureStream方法（实验特性，不同浏览器支持程度不高）返回的流；WebRTC中的remote流；自己创建的流等
      //注意：流内必须至少存在一条音轨(Audio Track)，比如audio标签必须等待到可以开始播放后才会有音轨，否则open会失败

      // audioTrackSet:{ deviceId:"",groupId:"", autoGainControl:true, echoCancellation:true, noiseSuppression:true }
      //普通麦克风录音时getUserMedia方法的audio配置参数，比如指定设备id，回声消除、降噪开关；注意：提供的任何配置值都不一定会生效
      //由于麦克风是全局共享的，所以新配置后需要close掉以前的再重新open

      // disableEnvInFix:false 内部参数，禁用设备卡顿时音频输入丢失补偿功能

      // takeoffEncodeChunk:NOOP //fn(chunkBytes) chunkBytes=[Uint8,...]：实时编码环境下接管编码器输出，当编码器实时编码出一块有效的二进制音频数据时实时回调此方法；参数为二进制的Uint8Array，就是编码出来的音频数据片段，所有的chunkBytes拼接在一起即为完整音频。本实现的想法最初由QQ2543775048提出
      //当提供此回调方法时，将接管编码器的数据输出，编码器内部将放弃存储生成的音频数据；环境要求比较苛刻：如果当前环境不支持实时编码处理，将在open时直接走fail逻辑
      //因此提供此回调后调用stop方法将无法获得有效的音频数据，因为编码器内没有音频数据，因此stop时返回的blob将是一个字节长度为0的blob
      //目前只有mp3格式实现了实时编码，在支持实时处理的环境中将会实时的将编码出来的mp3片段通过此方法回调，所有的chunkBytes拼接到一起即为完整的mp3，此种拼接的结果比mock方法实时生成的音质更加，因为天然避免了首尾的静默
      //目前除mp3外其他格式不可以提供此回调，提供了将在open时直接走fail逻辑
    };

    for (var k in set) {
      o[k] = set[k];
    }
    this.set = o;

    this._S = 9; //stop同步锁，stop可以阻止open过程中还未运行的start
    this.Sync = { O: 9, C: 9 }; //和Recorder.Sync一致，只不过这个是非全局的，仅用来简化代码逻辑，无实际作用
  }
  //同步锁，控制对Stream的竞争；用于close时中断异步的open；一个对象open如果变化了都要阻止close，Stream的控制权交个新的对象
  Recorder.Sync = { /*open*/ O: 9, /*close*/ C: 9 };

  Recorder.prototype = initFn.prototype = {
    CLog: CLog,

    //流相关的数据存储在哪个对象里面；如果提供了sourceStream，数据直接存储在当前对象中，否则存储在全局
    _streamStore: function () {
      if (this.set.sourceStream) {
        return this;
      } else {
        return Recorder;
      }
    },

    //打开录音资源True(),False(msg,isUserNotAllow)，需要调用close。注意：此方法是异步的；一般使用时打开，用完立即关闭；可重复调用，可用来测试是否能录音
    open: function (True, False) {
      var This = this,
        streamStore = This._streamStore();
      True = True || NOOP;
      var failCall = function (errMsg, isUserNotAllow) {
        isUserNotAllow = !!isUserNotAllow;
        This.CLog(
          '录音open失败：' + errMsg + ',isUserNotAllow:' + isUserNotAllow,
          1
        );
        False && False(errMsg, isUserNotAllow);
        throw new Error(errMsg);
      };

      var ok = function () {
        This.CLog('open ok id:' + This.id);
        True();

        This._SO = 0; //解除stop对open中的start调用的阻止
      };

      //同步锁
      var Lock = streamStore.Sync;
      var lockOpen = ++Lock.O,
        lockClose = Lock.C;
      This._O = This._O_ = lockOpen; //记住当前的open，如果变化了要阻止close，这里假定了新对象已取代当前对象并且不再使用
      This._SO = This._S; //记住open过程中的stop，中途任何stop调用后都不能继续open中的start
      var lockFail = function () {
        //允许多次open，但不允许任何一次close，或者自身已经调用了关闭
        if (lockClose != Lock.C || !This._O) {
          var err = 'open被取消';
          if (lockOpen == Lock.O) {
            //无新的open，已经调用了close进行取消，此处应让上次的close明确生效
            This.close();
          } else {
            err = 'open被中断';
          }
          failCall(err);
          return true;
        }
      };

      //环境配置检查
      var checkMsg = This.envCheck({ envName: 'H5', canProcess: true });
      if (checkMsg) {
        failCall('不能录音：' + checkMsg);
        return;
      }

      //***********已直接提供了音频流************
      if (This.set.sourceStream) {
        if (!Recorder.Support()) {
          failCall('不支持此浏览器从流中获取录音');
          return;
        }

        Disconnect(streamStore); //可能已open过，直接先尝试断开
        This.Stream = This.set.sourceStream;
        This.Stream._call = {};

        try {
          Connect(streamStore);
        } catch (e) {
          failCall('从流中打开录音失败：' + e.message);
          return;
        }
        ok();
        return;
      }

      //***********打开麦克风得到全局的音频流************
      var codeFail = function (code, msg) {
        try {
          //跨域的优先检测一下
          window.top.a;
        } catch (e) {
          failCall(
            '无权录音(跨域，请尝试给iframe添加麦克风访问策略，如allow="camera;microphone")'
          );
          return;
        }

        if (/Permission|Allow/i.test(code)) {
          failCall('未授权浏览器录音权限', true);
        } else if (window.isSecureContext === false) {
          failCall('浏览器禁止不安全页面录音，可开启https解决');
        } else if (/Found/i.test(code)) {
          //可能是非安全环境导致的没有设备
          failCall(msg + '，无可用麦克风');
        } else {
          failCall(msg);
        }
      };

      //如果已打开并且有效就不要再打开了
      if (Recorder.IsOpen()) {
        ok();
        return;
      }
      if (!Recorder.Support()) {
        codeFail('', '此浏览器不支持录音');
        return;
      }

      //请求权限，如果从未授权，一般浏览器会弹出权限请求弹框
      var f1 = function (stream) {
        //获取到的track.readyState!="live"，刚刚回调时可能是正常的，但过一下可能就被关掉了，原因不明。延迟一下保证真异步。对正常浏览器不影响
        setTimeout(function () {
          stream._call = {};
          var oldStream = Recorder.Stream;
          if (oldStream) {
            Disconnect(); //直接断开已存在的，旧的Connect未完成会自动终止
            stream._call = oldStream._call;
          }
          Recorder.Stream = stream;
          if (lockFail()) return;

          if (Recorder.IsOpen()) {
            if (oldStream) This.CLog('发现同时多次调用open', 1);

            Connect(streamStore, 1);
            ok();
          } else {
            failCall('录音功能无效：无音频流');
          }
        }, 100);
      };
      var f2 = function (e) {
        var code = e.name || e.message || e.code + ':' + e;
        This.CLog('请求录音权限错误', 1, e);

        codeFail(code, '无法录音：' + code);
        throw new Error(code);
      };

      var trackSet = {
        noiseSuppression: false, //默认禁用降噪，原声录制，免得移动端表现怪异（包括系统播放声音变小）
        echoCancellation: false, //回声消除
      };
      var trackSet2 = This.set.audioTrackSet;
      for (var k in trackSet2) trackSet[k] = trackSet2[k];
      trackSet.sampleRate = Recorder.Ctx.sampleRate; //必须指明采样率，不然手机上MediaRecorder采样率16k

      try {
        var pro = Recorder.Scope[getUserMediaTxt]({ audio: trackSet }, f1, f2);
      } catch (e) {
        //不能设置trackSet就算了
        This.CLog(getUserMediaTxt, 3, e);
        pro = Recorder.Scope[getUserMediaTxt]({ audio: true }, f1, f2);
      }
      if (pro && pro.then) {
        pro.then(f1)[True && 'catch'](f2); //fix 关键字，保证catch压缩时保持字符串形式
      }
    },
    //关闭释放录音资源
    close: function (call) {
      call = call || NOOP;

      var This = this,
        streamStore = This._streamStore();
      This._stop();

      var Lock = streamStore.Sync;
      This._O = 0;
      if (This._O_ != Lock.O) {
        //唯一资源Stream的控制权已交给新对象，这里不能关闭。此处在每次都弹权限的浏览器内可能存在泄漏，新对象被拒绝权限可能不会调用close，忽略这种不处理
        This.CLog(
          'close被忽略（因为同时open了多个rec，只有最后一个会真正close）',
          3
        );
        call();
        return;
      }
      Lock.C++; //获得控制权

      Disconnect(streamStore);

      This.CLog('close');
      call();
    },

    /*模拟一段录音数据，后面可以调用stop进行编码，需提供pcm数据[1,2,3...]，pcm的采样率*/
    mock: function (pcmData, pcmSampleRate) {
      var This = this;
      This._stop(); //清理掉已有的资源

      This.isMock = 1;
      This.mockEnvInfo = null;
      This.buffers = [pcmData];
      This.recSize = pcmData.length;
      This[srcSampleRateTxt] = pcmSampleRate;
      return This;
    },
    envCheck: function (envInfo) {
      //平台环境下的可用性检查，任何时候都可以调用检查，返回errMsg:""正常，"失败原因"
      //envInfo={envName:"H5",canProcess:true}
      var errMsg,
        This = this,
        set = This.set;

      //检测CPU的数字字节序，TypedArray字节序是个迷，直接拒绝罕见的大端模式，因为找不到这种CPU进行测试
      var tag = 'CPU_BE';
      if (
        !errMsg &&
        !Recorder[tag] &&
        window.Int8Array &&
        !new Int8Array(new Int32Array([1]).buffer)[0]
      ) {
        Traffic(tag); //如果开启了流量统计，这里将发送一个图片请求
        errMsg = '不支持' + tag + '架构';
      }

      //编码器检查环境下配置是否可用
      if (!errMsg) {
        var type = set.type;
        if (This[type + '_envCheck']) {
          //编码器已实现环境检查
          errMsg = This[type + '_envCheck'](envInfo, set);
        } else {
          //未实现检查的手动检查配置是否有效
          if (set.takeoffEncodeChunk) {
            errMsg =
              type +
              '类型' +
              (This[type] ? '' : '(未加载编码器)') +
              '不支持设置takeoffEncodeChunk';
          }
        }
      }

      return errMsg || '';
    },
    envStart: function (mockEnvInfo, sampleRate) {
      //平台环境相关的start调用
      var This = this,
        set = This.set;
      This.isMock = mockEnvInfo ? 1 : 0; //非H5环境需要启用mock，并提供envCheck需要的环境信息
      This.mockEnvInfo = mockEnvInfo;
      This.buffers = []; //数据缓冲
      This.recSize = 0; //数据大小

      This.envInLast = 0; //envIn接收到最后录音内容的时间
      This.envInFirst = 0; //envIn接收到的首个录音内容的录制时间
      This.envInFix = 0; //补偿的总时间
      This.envInFixTs = []; //补偿计数列表

      //engineCtx需要提前确定最终的采样率
      var setSr = set[sampleRateTxt];
      if (setSr > sampleRate) {
        set[sampleRateTxt] = sampleRate;
      } else {
        setSr = 0;
      }
      This[srcSampleRateTxt] = sampleRate;
      This.CLog(
        srcSampleRateTxt +
          ': ' +
          sampleRate +
          ' set.' +
          sampleRateTxt +
          ': ' +
          set[sampleRateTxt] +
          (setSr ? ' 忽略' + setSr : ''),
        setSr ? 3 : 0
      );

      This.engineCtx = 0;
      //此类型有边录边转码(Worker)支持
      if (This[set.type + '_start']) {
        var engineCtx = (This.engineCtx = This[set.type + '_start'](set));
        if (engineCtx) {
          engineCtx.pcmDatas = [];
          engineCtx.pcmSize = 0;
        }
      }
    },
    envResume: function () {
      //和平台环境无关的恢复录音
      //重新开始计数
      this.envInFixTs = [];
    },
    envIn: function (pcm, sum) {
      //和平台环境无关的pcm[Int16]输入
      var This = this,
        set = This.set,
        engineCtx = This.engineCtx;
      var bufferSampleRate = This[srcSampleRateTxt];
      var size = pcm.length;
      var powerLevel = Recorder.PowerLevel(sum, size);

      var buffers = This.buffers;
      var bufferFirstIdx = buffers.length; //之前的buffer都是经过onProcess处理好的，不允许再修改
      buffers.push(pcm);

      //有engineCtx时会被覆盖，这里保存一份
      var buffersThis = buffers;
      var bufferFirstIdxThis = bufferFirstIdx;

      //卡顿丢失补偿：因为设备很卡的时候导致H5接收到的数据量不够造成播放时候变速，结果比实际的时长要短，此处保证了不会变短，但不能修复丢失的音频数据造成音质变差。当前算法采用输入时间侦测下一帧是否需要添加补偿帧，需要(6次输入||超过1秒)以上才会开始侦测，如果滑动窗口内丢失超过1/3就会进行补偿
      var now = Date.now();
      var pcmTime = Math.round((size / bufferSampleRate) * 1000);
      This.envInLast = now;
      if (This.buffers.length == 1) {
        //记下首个录音数据的录制时间
        This.envInFirst = now - pcmTime;
      }
      var envInFixTs = This.envInFixTs;
      envInFixTs.splice(0, 0, { t: now, d: pcmTime });
      //保留3秒的计数滑动窗口，另外超过3秒的停顿不补偿
      var tsInStart = now,
        tsPcm = 0;
      for (var i = 0; i < envInFixTs.length; i++) {
        var o = envInFixTs[i];
        if (now - o.t > 3000) {
          envInFixTs.length = i;
          break;
        }
        tsInStart = o.t;
        tsPcm += o.d;
      }
      //达到需要的数据量，开始侦测是否需要补偿
      var tsInPrev = envInFixTs[1];
      var tsIn = now - tsInStart;
      var lost = tsIn - tsPcm;
      if (
        lost > tsIn / 3 &&
        ((tsInPrev && tsIn > 1000) || envInFixTs.length >= 6)
      ) {
        //丢失过多，开始执行补偿
        var addTime = now - tsInPrev.t - pcmTime; //距离上次输入丢失这么多ms
        if (addTime > pcmTime / 5) {
          //丢失超过本帧的1/5
          var fixOpen = !set.disableEnvInFix;
          This.CLog(
            '[' + now + ']' + (fixOpen ? '' : '未') + '补偿' + addTime + 'ms',
            3
          );
          This.envInFix += addTime;

          //用静默进行补偿
          if (fixOpen) {
            var addPcm = new Int16Array((addTime * bufferSampleRate) / 1000);
            size += addPcm.length;
            buffers.push(addPcm);
          }
        }
      }

      var sizeOld = This.recSize,
        addSize = size;
      var bufferSize = sizeOld + addSize;
      This.recSize = bufferSize; //此值在onProcess后需要修正，可能新数据被修改

      //此类型有边录边转码(Worker)支持，开启实时转码
      if (engineCtx) {
        //转换成set的采样率
        var chunkInfo = Recorder.SampleData(
          buffers,
          bufferSampleRate,
          set[sampleRateTxt],
          engineCtx.chunkInfo
        );
        engineCtx.chunkInfo = chunkInfo;

        sizeOld = engineCtx.pcmSize;
        addSize = chunkInfo.data.length;
        bufferSize = sizeOld + addSize;
        engineCtx.pcmSize = bufferSize; //此值在onProcess后需要修正，可能新数据被修改

        buffers = engineCtx.pcmDatas;
        bufferFirstIdx = buffers.length;
        buffers.push(chunkInfo.data);
        bufferSampleRate = chunkInfo[sampleRateTxt];
      }

      var duration = Math.round((bufferSize / bufferSampleRate) * 1000);
      var bufferNextIdx = buffers.length;
      var bufferNextIdxThis = buffersThis.length;

      //允许异步处理buffer数据
      var asyncEnd = function () {
        //重新计算size，异步的早已减去添加的，同步的需去掉本次添加的然后重新计算
        var num = asyncBegin ? 0 : -addSize;
        var hasClear = buffers[0] == null;
        for (var i = bufferFirstIdx; i < bufferNextIdx; i++) {
          var buffer = buffers[i];
          if (buffer == null) {
            //已被主动释放内存，比如长时间实时传输录音时
            hasClear = 1;
          } else {
            num += buffer.length;

            //推入后台边录边转码
            if (engineCtx && buffer.length) {
              This[set.type + '_encode'](engineCtx, buffer);
            }
          }
        }

        //同步清理This.buffers，不管buffers到底清了多少个，buffersThis是使用不到的进行全清
        if (hasClear && engineCtx) {
          var i = bufferFirstIdxThis;
          if (buffersThis[0]) {
            i = 0;
          }
          for (; i < bufferNextIdxThis; i++) {
            buffersThis[i] = null;
          }
        }

        //统计修改后的size，如果异步发生clear要原样加回来，同步的无需操作
        if (hasClear) {
          num = asyncBegin ? addSize : 0;

          buffers[0] = null; //彻底被清理
        }
        if (engineCtx) {
          engineCtx.pcmSize += num;
        } else {
          This.recSize += num;
        }
      };
      //实时回调处理数据，允许修改或替换上次回调以来新增的数据 ，但是不允许修改已处理过的，不允许增删第一维数组 ，允许将第二维数组任意修改替换成空数组也可以
      var asyncBegin = 0,
        procTxt = 'rec.set.onProcess';
      try {
        asyncBegin = set.onProcess(
          buffers,
          powerLevel,
          duration,
          bufferSampleRate,
          bufferFirstIdx,
          asyncEnd
        );
      } catch (e) {
        //此错误显示不要用CLog，这样控制台内相同内容不会重复打印
        console.error(procTxt + '回调出错是不允许的，需保证不会抛异常', e);
      }

      var slowT = Date.now() - now;
      if (slowT > 10 && This.envInFirst - now > 1000) {
        //1秒后开始onProcess性能监测
        This.CLog(procTxt + '低性能，耗时' + slowT + 'ms', 3);
      }

      if (asyncBegin === true) {
        //开启了异步模式，onProcess已接管buffers新数据，立即清空，避免出现未处理的数据
        var hasClear = 0;
        for (var i = bufferFirstIdx; i < bufferNextIdx; i++) {
          if (buffers[i] == null) {
            //已被主动释放内存，比如长时间实时传输录音时 ，但又要开启异步模式，此种情况是非法的
            hasClear = 1;
          } else {
            buffers[i] = new Int16Array(0);
          }
        }

        if (hasClear) {
          This.CLog('未进入异步前不能清除buffers', 3);
        } else {
          //还原size，异步结束后再统计仅修改后的size，如果发生clear要原样加回来
          if (engineCtx) {
            engineCtx.pcmSize -= addSize;
          } else {
            This.recSize -= addSize;
          }
        }
      } else {
        asyncEnd();
      }
    },

    //开始录音，需先调用open；只要open成功时，调用此方法是安全的，如果未open强行调用导致的内部错误将不会有任何提示，stop时自然能得到错误
    start: function () {
      var This = this,
        ctx = Recorder.Ctx;

      var isOpen = 1;
      if (This.set.sourceStream) {
        //直接提供了流，仅判断是否调用了open
        if (!This.Stream) {
          isOpen = 0;
        }
      } else if (!Recorder.IsOpen()) {
        //监测全局麦克风是否打开并且有效
        isOpen = 0;
      }
      if (!isOpen) {
        This.CLog('未open', 1);
        return;
      }
      This.CLog('开始录音');

      This._stop();
      This.state = 3; //0未录音 1录音中 2暂停 3等待ctx激活
      This.envStart(null, ctx[sampleRateTxt]);

      //检查open过程中stop是否已经调用过
      if (This._SO && This._SO + 1 != This._S) {
        //上面调用过一次 _stop
        //open未完成就调用了stop，此种情况终止start。也应尽量避免出现此情况
        This.CLog('start被中断', 3);
        return;
      }
      This._SO = 0;

      var end = function () {
        if (This.state == 3) {
          This.state = 1;
          This.resume();
        }
      };
      if (ctx.state == 'suspended') {
        This.CLog('wait ctx resume...');
        ctx.resume().then(function () {
          This.CLog('ctx resume');
          end();
        });
      } else {
        end();
      }
    },
    /*暂停录音*/
    pause: function () {
      var This = this;
      if (This.state) {
        This.state = 2;
        This.CLog('pause');
        delete This._streamStore().Stream._call[This.id];
      }
    },
    /*恢复录音*/
    resume: function () {
      var This = this;
      if (This.state) {
        This.state = 1;
        This.CLog('resume');
        This.envResume();

        var stream = This._streamStore().Stream;
        stream._call[This.id] = function (pcm, sum) {
          if (This.state == 1) {
            This.envIn(pcm, sum);
          }
        };
        ConnAlive(stream); //AudioWorklet只会在ctx激活后运行
      }
    },

    _stop: function (keepEngine) {
      var This = this,
        set = This.set;
      if (!This.isMock) {
        This._S++;
      }
      if (This.state) {
        This.pause();
        This.state = 0;
      }
      if (!keepEngine && This[set.type + '_stop']) {
        This[set.type + '_stop'](This.engineCtx);
        This.engineCtx = 0;
      }
    },
    /*
	结束录音并返回录音数据blob对象
		True(blob,duration) blob：录音数据audio/mp3|wav格式
							duration：录音时长，单位毫秒
		False(msg)
		autoClose:false 可选，是否自动调用close，默认为false
	*/
    stop: function (True, False, autoClose) {
      var This = this,
        set = This.set,
        t1;
      var envInMS = This.envInLast - This.envInFirst,
        envInLen = envInMS && This.buffers.length; //可能未start
      This.CLog(
        'stop 和start时差' +
          (envInMS
            ? envInMS +
              'ms 补偿' +
              This.envInFix +
              'ms' +
              ' envIn:' +
              envInLen +
              ' fps:' +
              ((envInLen / envInMS) * 1000).toFixed(1)
            : '-')
      );

      var end = function () {
        This._stop(); //彻底关掉engineCtx
        if (autoClose) {
          This.close();
        }
      };
      var err = function (msg) {
        This.CLog('结束录音失败：' + msg, 1);
        False && False(msg);
        end();
      };
      var ok = function (blob, duration, originBUffers, originSampleRate) {
        This.CLog(
          '结束录音 编码花' +
            (Date.now() - t1) +
            'ms 音频时长' +
            duration +
            'ms 文件大小' +
            blob.size +
            'b'
        );
        if (set.takeoffEncodeChunk) {
          //接管了输出，此时blob长度为0
          This.CLog(
            '启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据',
            3
          );
        } else if (blob.size < Math.max(100, duration / 2)) {
          //1秒小于0.5k？
          err('生成的' + set.type + '无效');
          return;
        }
        True &&
          True(blob, duration, originBUffers || [], originSampleRate || 44100);
        end();
      };
      if (!This.isMock) {
        var isCtxWait = This.state == 3;
        if (!This.state || isCtxWait) {
          err(
            '未开始录音' +
              (isCtxWait ? '，开始录音前无用户交互导致AudioContext未运行' : '')
          );
          return;
        }
        This._stop(true);
      }
      var size = This.recSize;
      if (!size) {
        err('未采集到录音');
        return;
      }
      if (!This.buffers[0]) {
        err('音频buffers被释放');
        return;
      }
      if (!This[set.type]) {
        err('未加载' + set.type + '编码器');
        return;
      }

      //环境配置检查，此处仅针对mock调用，因为open已经检查过了
      if (This.isMock) {
        var checkMsg = This.envCheck(
          This.mockEnvInfo || { envName: 'mock', canProcess: false }
        ); //没有提供环境信息的mock时没有onProcess回调
        if (checkMsg) {
          err('录音错误：' + checkMsg);
          return;
        }
      }

      //此类型有边录边转码(Worker)支持
      var engineCtx = This.engineCtx;
      if (This[set.type + '_complete'] && engineCtx) {
        var duration = Math.round(
          (engineCtx.pcmSize / set[sampleRateTxt]) * 1000
        ); //采用后的数据长度和buffers的长度可能微小的不一致，是采样率连续转换的精度问题

        t1 = Date.now();
        This[set.type + '_complete'](
          engineCtx,
          function (blob) {
            ok(blob, duration);
          },
          err
        );
        return;
      }

      //标准UI线程转码，调整采样率
      t1 = Date.now();
      var chunk = Recorder.SampleData(
        This.buffers,
        This[srcSampleRateTxt],
        set[sampleRateTxt]
      );

      set[sampleRateTxt] = chunk[sampleRateTxt];
      var res = chunk.data;
      var duration = Math.round((res.length / set[sampleRateTxt]) * 1000);
      This.CLog(
        '采样' + size + '->' + res.length + ' 花:' + (Date.now() - t1) + 'ms'
      );

      setTimeout(function () {
        t1 = Date.now();
        This[set.type](
          res,
          function (blob) {
            ok(blob, duration, This.buffers, This[srcSampleRateTxt]);
          },
          function (msg) {
            err(msg);
          }
        );
      });
    },
  };

  if (window[RecTxt]) {
    CLog('重复引入' + RecTxt, 3);
    window[RecTxt].Destroy();
  }
  window[RecTxt] = Recorder;

  //=======从WebM字节流中提取pcm数据，提取成功返回Float32Array，失败返回null||-1=====
  var WebM_Extract = function (inBytes, scope) {
    if (!scope.pos) {
      scope.pos = [0];
      scope.tracks = {};
      scope.bytes = [];
    }
    var tracks = scope.tracks,
      position = [scope.pos[0]];
    var endPos = function () {
      scope.pos[0] = position[0];
    };

    var sBL = scope.bytes.length;
    var bytes = new Uint8Array(sBL + inBytes.length);
    bytes.set(scope.bytes);
    bytes.set(inBytes, sBL);
    scope.bytes = bytes;

    //先读取文件头和Track信息
    if (!scope._ht) {
      readMatroskaVInt(bytes, position); //EBML Header
      readMatroskaBlock(bytes, position); //跳过EBML Header内容
      if (
        !BytesEq(readMatroskaVInt(bytes, position), [0x18, 0x53, 0x80, 0x67])
      ) {
        return; //未识别到Segment
      }
      readMatroskaVInt(bytes, position); //跳过Segment长度值
      while (position[0] < bytes.length) {
        var eid0 = readMatroskaVInt(bytes, position);
        var bytes0 = readMatroskaBlock(bytes, position);
        var pos0 = [0],
          audioIdx = 0;
        if (!bytes0) return; //数据不全，等待缓冲
        //Track完整数据，循环读取TrackEntry
        if (BytesEq(eid0, [0x16, 0x54, 0xae, 0x6b])) {
          while (pos0[0] < bytes0.length) {
            var eid1 = readMatroskaVInt(bytes0, pos0);
            var bytes1 = readMatroskaBlock(bytes0, pos0);
            var pos1 = [0],
              track = { channels: 0, sampleRate: 0 };
            if (BytesEq(eid1, [0xae])) {
              //TrackEntry
              while (pos1[0] < bytes1.length) {
                var eid2 = readMatroskaVInt(bytes1, pos1);
                var bytes2 = readMatroskaBlock(bytes1, pos1);
                var pos2 = [0];
                if (BytesEq(eid2, [0xd7])) {
                  //Track Number
                  var val = BytesInt(bytes2);
                  track.number = val;
                  tracks[val] = track;
                } else if (BytesEq(eid2, [0x83])) {
                  //Track Type
                  var val = BytesInt(bytes2);
                  if (val == 1) track.type = 'video';
                  else if (val == 2) {
                    track.type = 'audio';
                    if (!audioIdx) scope.track0 = track;
                    track.idx = audioIdx++;
                  } else track.type = 'Type-' + val;
                } else if (BytesEq(eid2, [0x86])) {
                  //Track Codec
                  var str = '';
                  for (var i = 0; i < bytes2.length; i++) {
                    str += String.fromCharCode(bytes2[i]);
                  }
                  track.codec = str;
                } else if (BytesEq(eid2, [0xe1])) {
                  while (pos2[0] < bytes2.length) {
                    //循环读取 Audio 属性
                    var eid3 = readMatroskaVInt(bytes2, pos2);
                    var bytes3 = readMatroskaBlock(bytes2, pos2);
                    //采样率、位数、声道数
                    if (BytesEq(eid3, [0xb5])) {
                      var val = 0,
                        arr = new Uint8Array(bytes3.reverse()).buffer;
                      if (bytes3.length == 4) val = new Float32Array(arr)[0];
                      else if (bytes3.length == 8)
                        val = new Float64Array(arr)[0];
                      else CLog('WebM Track !Float', 1, bytes3);
                      track[sampleRateTxt] = Math.round(val);
                    } else if (BytesEq(eid3, [0x62, 0x64]))
                      track.bitDepth = BytesInt(bytes3);
                    else if (BytesEq(eid3, [0x9f]))
                      track.channels = BytesInt(bytes3);
                  }
                }
              }
            }
          }
          scope._ht = 1;
          CLog('WebM Tracks', tracks);
          endPos();
          break;
        }
      }
    }

    //校验音频参数信息，如果不符合代码要求，统统拒绝处理
    var track0 = scope.track0;
    if (!track0) return;
    if (track0.bitDepth == 16 && /FLOAT/i.test(track0.codec)) {
      track0.bitDepth = 32; //chrome v66 实际为浮点数
      CLog('WebM 16改32位', 3);
    }
    if (
      track0[sampleRateTxt] != scope[sampleRateTxt] ||
      track0.bitDepth != 32 ||
      track0.channels < 1 ||
      !/(\b|_)PCM\b/i.test(track0.codec)
    ) {
      scope.bytes = []; //格式非预期 无法处理，清空缓冲数据
      if (!scope.bad) CLog('WebM Track非预期', 3, scope);
      scope.bad = 1;
      return -1;
    }

    //循环读取Cluster内的SimpleBlock
    var datas = [],
      dataLen = 0;
    while (position[0] < bytes.length) {
      var eid1 = readMatroskaVInt(bytes, position);
      var bytes1 = readMatroskaBlock(bytes, position);
      if (!bytes1) break; //数据不全，等待缓冲
      if (BytesEq(eid1, [0xa3])) {
        //SimpleBlock完整数据
        var trackNo = bytes1[0] & 0xf;
        var track = tracks[trackNo];
        if (track.idx === 0) {
          var u8arr = new Uint8Array(bytes1.length - 4);
          for (var i = 4; i < bytes1.length; i++) {
            u8arr[i - 4] = bytes1[i];
          }
          datas.push(u8arr);
          dataLen += u8arr.length;
        }
      }
      endPos();
    }

    if (dataLen) {
      var more = new Uint8Array(bytes.length - scope.pos[0]);
      more.set(bytes.subarray(scope.pos[0]));
      scope.bytes = more; //清理已读取了的缓冲数据
      scope.pos[0] = 0;

      var u8arr = new Uint8Array(dataLen); //已获取的音频数据
      for (var i = 0, i2 = 0; i < datas.length; i++) {
        u8arr.set(datas[i], i2);
        i2 += datas[i].length;
      }
      var arr = new Float32Array(u8arr.buffer);

      if (track0.channels > 1) {
        //多声道，提取一个声道
        var arr2 = [];
        for (var i = 0; i < arr.length; ) {
          arr2.push(arr[i]);
          i += track0.channels;
        }
        arr = new Float32Array(arr2);
      }
      return arr;
    }
  };
  //两个字节数组内容是否相同
  var BytesEq = function (bytes1, bytes2) {
    if (!bytes1 || bytes1.length != bytes2.length) return false;
    if (bytes1.length == 1) return bytes1[0] == bytes2[0];
    for (var i = 0; i < bytes1.length; i++) {
      if (bytes1[i] != bytes2[i]) return false;
    }
    return true;
  };
  //字节数组BE转成int数字
  var BytesInt = function (bytes) {
    var s = ''; //0-8字节，js位运算只支持4字节
    for (var i = 0; i < bytes.length; i++) {
      var n = bytes[i];
      s += (n < 16 ? '0' : '') + n.toString(16);
    }
    return parseInt(s, 16) || 0;
  };
  //读取一个可变长数值字节数组
  var readMatroskaVInt = function (arr, pos, trim) {
    var i = pos[0];
    if (i >= arr.length) return;
    var b0 = arr[i],
      b2 = ('0000000' + b0.toString(2)).substr(-8);
    var m = /^(0*1)(\d*)$/.exec(b2);
    if (!m) return;
    var len = m[1].length,
      val = [];
    if (i + len > arr.length) return;
    for (var i2 = 0; i2 < len; i2++) {
      val[i2] = arr[i];
      i++;
    }
    if (trim) val[0] = parseInt(m[2] || '0', 2);
    pos[0] = i;
    return val;
  };
  //读取一个自带长度的内容字节数组
  var readMatroskaBlock = function (arr, pos) {
    var lenVal = readMatroskaVInt(arr, pos, 1);
    if (!lenVal) return;
    var len = BytesInt(lenVal);
    var i = pos[0],
      val = [];
    if (len < 0x7fffffff) {
      //超大值代表没有长度
      if (i + len > arr.length) return;
      for (var i2 = 0; i2 < len; i2++) {
        val[i2] = arr[i];
        i++;
      }
    }
    pos[0] = i;
    return val;
  };
});
